﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace ExchangeHog.MessageParser
{
	//
	//	                                            ; (  Octal, Decimal.)
	//	CHAR        =  <any ASCII character>        ; (  0-177,  0.-127.)
	//	ALPHA       =  <any ASCII alphabetic character>
	//	                                            ; (101-132, 65.- 90.)
	//	                                            ; (141-172, 97.-122.)
	//	DIGIT       =  <any ASCII decimal digit>    ; ( 60- 71, 48.- 57.)
	//	CTL         =  <any ASCII control           ; (  0- 37,  0.- 31.)
	//	                character and DEL>          ; (    177,     127.)
	//	CR          =  <ASCII CR, carriage return>  ; (     15,      13.)
	//	LF          =  <ASCII LF, linefeed>         ; (     12,      10.)
	//	SPACE       =  <ASCII SP, space>            ; (     40,      32.)
	//	HTAB        =  <ASCII HT, horizontal-tab>   ; (     11,       9.)
	//	<">         =  <ASCII quote mark>           ; (     42,      34.)
	//	CRLF        =  CR LF
	//	LWSP-char   =  SPACE / HTAB                 ; semantics = SPACE
	//				
	//	linear-white-space =  1*([CRLF] LWSP-char)  ; semantics = SPACE
	//	                                            ; CRLF => folding
	//	
	//	specials    =  "(" / ")" / "<" / ">" / "@"  ; Must be in quoted-
	//	            /  "," / ";" / ":" / "\" / <">  ;  string, to use
	//	            /  "." / "[" / "]"              ;  within a word.
	//	
	//	delimiters  =  specials / linear-white-space / comment
	//	
	//	text        =  <any CHAR, including bare    ; => atoms, specials,
	//	                CR & bare LF, but NOT       ;  comments and
	//	                including CRLF>             ;  quoted-strings are
	//	                                            ;  NOT recognized.
	//	
	//	atom        =  1*<any CHAR except specials, SPACE and CTLs>
	//	
	//	quoted-string = <"> *(qtext/quoted-pair) <">; Regular qtext or
	//	                                            ;   quoted chars.
	//	
	//	qtext       =  <any CHAR excepting <">,     ; => may be folded
	//	                "\" & CR, and including
	//	                linear-white-space>
	//	
	//	domain-literal =  "[" *(dtext / quoted-pair) "]"
	//	
	//	dtext       =  <any CHAR excluding "[",     ; => may be folded
	//	                "]", "\" & CR, & including
	//	                linear-white-space>
	//	
	//	comment     =  "(" *(ctext / quoted-pair / comment) ")"
	//	
	//	ctext       =  <any CHAR excluding "(",     ; => may be folded
	//	                ")", "\" & CR, & including
	//	                linear-white-space>
	//	
	//	quoted-pair =  "\" CHAR                     ; may quote any char
	//	
	//	phrase      =  1*word                       ; Sequence of words
	//	
	//	word        =  atom / quoted-string
	//
	class StructuredFieldToken: BaseToken
	{
		internal enum TokenType { NONE, COMMENT, QUOTEDSTRING, ROUTINGINFORMATION, DOMAINLITERAL, SPACE, CRLF, PERIOD, AT, COMMA, COLON, ATOM, SEMICOLON, EOF, SLASH };
		internal TokenType TType { get; private set; }
		public String Repr { get; internal set; }
		internal int TokenTypeAsInt() { return (int)TType; }

		internal StructuredFieldToken(TokenType tt = TokenType.NONE, string repr = null) { TType = tt; Repr = repr; }

		internal void MarkAsCRLF() { TType = TokenType.CRLF; }
		internal void MarkAsSPACE() { TType = TokenType.SPACE; }

		internal bool IsSPACE() { return TType == TokenType.SPACE || TType == TokenType.COMMENT; }
	}

	class AddressLexer : BaseLexer<StructuredFieldToken>
	{
		void ExpectLWS()
		{
			Char ch = ReadChar();
			Expect(delegate() { return (ch == ' ' || ch == '\t'); }, ch, "space or tab");
		}

		private void ParseQuotedString()
		{
			Char ch = ReadChar();
			Expect(delegate() { return (ch == '\"'); }, ch, "quote '\"'");

			StringBuilder sb = new StringBuilder();
			bool isEndOfQuotedStringReached = false;
			while (!IsEndOfStream() && !isEndOfQuotedStringReached)
			{
				ch = ReadChar();
				switch (ch)
				{
					case '\"':
						isEndOfQuotedStringReached = true;
						break;
					case '\\':
						ch = ReadChar();
						if (ch == '\r')
						{
							ch = PeekChar();
							if (ch == '\n')
							{
								sb.Append("\n"); // CRLF is quoted - add it to the string's body
								SkipChar();
								ExpectLWS();
							}
						}
						else
							sb.Append(ch);
						break;
					default:
						sb.Append(ch);
						break;
				}
			}
			Tokens.Enqueue(new StructuredFieldToken(StructuredFieldToken.TokenType.QUOTEDSTRING, sb.ToString()));
		}

		private void ParseComment()
		{
			Char ch = ReadChar();
			Expect(delegate() { return (ch == '('); }, ch, "left parenthesis '('");

			StringBuilder sb = new StringBuilder();
			int depth = 1;
			bool isEndOfCommentReached = false;
			while (!IsEndOfStream() && !isEndOfCommentReached)
			{
				ch = ReadChar();
				switch (ch)
				{
					case ')':
						if ((--depth) == 0)
							isEndOfCommentReached = true;
						break;
					case '(':
						depth++;
						break;
					case '\\': // quoted character
						ch = ReadChar();
						if (ch == '\r')
						{
							ch = PeekChar();
							if (ch == '\n')
							{
								sb.Append("\n"); // CRLF is quoted - add it to the comment's body
								SkipChar();
								ExpectLWS();
							}
						}
						else
							sb.Append(ch);
						break;
					default:
						sb.Append(ch);
						break;
				}
			}
			Tokens.Enqueue(new StructuredFieldToken(StructuredFieldToken.TokenType.COMMENT, sb.ToString()));
		} // end of ParseComment

		//
		// Angle brackets ("<" and ">")  are  generally  used  to
		// indicate  the  presence of a one machine-usable refer-
		// ence (e.g., delimiting mailboxes), possibly  including
		// source-routing to the machine.
		//
		private void ParseRoutingInformation()
		{
			Char ch = ReadChar();
			Expect(delegate() { return (ch == '<'); }, ch, "left angle bracket '<'");

			StringBuilder sb = new StringBuilder();
			bool isEndOfRoutingInformationReached = false;
			while (!IsEndOfStream() && !isEndOfRoutingInformationReached)
			{
				ch = ReadChar();
				switch (ch)
				{
					case '>': isEndOfRoutingInformationReached = true; break;
					default:
						sb.Append(ch);
						break;
				}
			}
			Tokens.Enqueue(new StructuredFieldToken(StructuredFieldToken.TokenType.ROUTINGINFORMATION, sb.ToString()));
		} // end of ParseRoutingInformation

		//
		// Square brackets ("[" and "]") are used to indicate the
		// presence  of  a  domain-literal, which the appropriate
		// name-domain  is  to  use  directly,  bypassing  normal
		// name-resolution mechanisms.
		//
		private void ParseDomainLiteral()
		{
			Char ch = ReadChar();
			Expect(delegate() { return (ch == '['); }, ch, "left square bracket '['");

			StringBuilder sb = new StringBuilder();
			bool isEndOfDomainLiteralReached = false;
			while (!IsEndOfStream() && !isEndOfDomainLiteralReached)
			{
				ch = ReadChar();
				switch (ch)
				{
					case ']': isEndOfDomainLiteralReached = true; break;
					case '\\': // quoted character
						ch = ReadChar();
						if (ch == '\r')
						{
							ch = PeekChar();
							if (ch == '\n')
							{
								sb.Append("\n"); // CRLF is quoted - add it to the domain literal
								SkipChar();
								ExpectLWS();
							}
						}
						else
							sb.Append(ch);
						break;
					default:
						sb.Append(ch);
						break;
				}
			}
			Tokens.Enqueue(new StructuredFieldToken(StructuredFieldToken.TokenType.DOMAINLITERAL, sb.ToString()));
		} // end of ParseDomainLiteral

		//	CHAR        =  <any ASCII character>        ; (  0-177,  0.-127.)
		//	CTL         =  <any ASCII control           ; (  0- 37,  0.- 31.)
		//	                character and DEL>          ; (    177,     127.)
		//	SPACE       =  <ASCII SP, space>            ; (     40,      32.)
		//	specials    =  "(" / ")" / "<" / ">" / "@"  ; Must be in quoted-
		//	            /  "," / ";" / ":" / "\" / <">  ;  string, to use
		//	            /  "." / "[" / "]"              ;  within a word.
		//	atom        =  1*<any CHAR except specials, SPACE and CTLs> /
		//					encoded-word				; from RFC 2047 (see below)
		//
		//		from RFC 2047
		//
		//	encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
		//	charset = token    ; see section 3
		//	encoding = token   ; see section 4
		//	token = 1*<Any CHAR except SPACE, CTLs, and especials>
		//	especials = specials / "?" / "="
		//	encoded-text = 1*<Any printable ASCII character other than "?" or SPACE>
		//

		static bool DoesBelongToAtom(Char ch)
		{
			return
				31 < ((short)ch) && ((short)ch) < 127 &&
				ch != ' ' && ch != '\t' &&
				ch != '(' && ch != ')' && ch != '<' && ch != '>' && ch != '@' &&
				ch != ',' && ch != ';' && ch != ':' && ch != '\\' && ch != '\"' &&
				ch != '.' && ch != '[' && ch != ']';
		}

		static bool DoesBelongToEncodedText(Char ch)
		{
			return DoesBelongToAtom(ch) && ch != '?' && ch != '=';
		}

		private String ParseEToken()
		{
			StringBuilder sb = new StringBuilder();
			Char ch = PeekChar();
			while (!IsEndOfStream() && DoesBelongToEncodedText(ch))
			{
				ReadChar();
				sb.Append(ch);
			}
			return sb.ToString();
		}

		private void ParseAtom()
		{
			String atomrepr;

			Char ch = ReadChar();
			if (ch == '=' && PeekChar() == '?')
			{
				//
				//	encoded word
				//
				ReadChar(); // skip '?'
				//
				//	charset
				//
				string charset = ParseEToken();
				ch = ReadChar();
				Expect(delegate() { return ch == '?'; }, ch, "'?'");
				//
				//	encoding
				//
				string encoding = ParseEToken();
				ch = ReadChar();
				Expect(delegate() { return ch == '?'; }, ch, "'?'");

				StringBuilder sb = new StringBuilder();
				ch = ReadChar();
				while (!IsEndOfStream() && ch != '?' && ch != ' ')
				{
					sb.Append(ch);
					ch = ReadChar();
				}

				Expect(delegate() { return ch == '?'; }, ch, "'?'");
				ch = ReadChar();
				Expect(delegate() { return ch == '='; }, ch, "'?'");

				atomrepr = AddressLexer.Decode(sb.ToString(), charset, encoding);
			}
			else
			{
				Expect(
					delegate()
					{
						return DoesBelongToAtom(ch);
					}, ch, "any char except specials, space and ctls");

				StringBuilder sb = new StringBuilder();
				sb.Append(ch);

				if (!IsEndOfStream())
				{
					ch = PeekChar();

					while (!IsEndOfStream() && DoesBelongToAtom(ch))
					{
						sb.Append(ch);
						SkipChar();
						if (!IsEndOfStream())
							ch = PeekChar();
					}
				}
				atomrepr = sb.ToString();
			}
			Tokens.Enqueue(new StructuredFieldToken(StructuredFieldToken.TokenType.ATOM, atomrepr));
		} // end of ParseAtom

		private static Char DecodeQuotedPrintableChar(string s, Decoder decoder)
		{
			byte code = Byte.Parse(s.Substring(1, 2));
			char[] tchars = new char[1];
			decoder.GetChars(new byte[] { code }, 0, 1, tchars, 0, true);
			return tchars[0];
		}

		public static string Decode(string s, string charset, string encoding)
		{
			string result;
			if (String.Compare(encoding, "q", true) == 0) // compare ignoring case
			{
				//
				//	QUOTED-PRINTABLE encoding
				//
				Decoder decoder = Encoding.GetEncoding(charset).GetDecoder();

				StringBuilder sb = new StringBuilder();
				int lastpeq = 0;
				int peq = s.IndexOf('=');
				while (peq >= 0)
				{
					sb.Append(s.Substring(lastpeq, peq - lastpeq + 1));
					sb.Append(DecodeQuotedPrintableChar(s.Substring(peq, 3), decoder));
					lastpeq = peq;
					if (lastpeq < s.Length)
						peq = s.IndexOf('=', lastpeq + 1);
					else
						peq = -1;
				}
				if (peq < s.Length - 1)
					sb.Append(s.Substring(peq + 1));
				result = sb.ToString();
			}
			else if (String.Compare(encoding, "b", true) == 0)
			{
				//
				//	BASE64 encoding
				//
				try
				{
					System.Text.Decoder utf8decoder = Encoding.GetEncoding(charset).GetDecoder();

					byte[] sbytes = Convert.FromBase64String(s);
					int tcharscount = utf8decoder.GetCharCount(sbytes, 0, sbytes.Length);
					char[] tchars = new char[tcharscount];
					utf8decoder.GetChars(sbytes, 0, sbytes.Length, tchars, 0);
					result = new String(tchars);
				}
				catch (Exception e)
				{
					throw new ApplicationException("Failed to decode base64 content" + e.Message);
				}

			}
			else
				throw new ApplicationException("Unsupported encoding: '" + encoding + "'");
			return result;
		} // end of Decode

		public StructuredFieldToken NextToken()
		{
			if (Tokens.Count == 0)
				do
					if (!IsEndOfStream())
					{
						Char ch = PeekChar();
						switch (ch)
						{
							case '@': SkipChar(); Tokens.Enqueue(new StructuredFieldToken(StructuredFieldToken.TokenType.AT, Char.ToString(ch))); break;
							case '.': SkipChar(); Tokens.Enqueue(new StructuredFieldToken(StructuredFieldToken.TokenType.PERIOD, Char.ToString(ch))); break;
							case ',': SkipChar(); Tokens.Enqueue(new StructuredFieldToken(StructuredFieldToken.TokenType.COMMA, Char.ToString(ch))); break;
							case ':': SkipChar(); Tokens.Enqueue(new StructuredFieldToken(StructuredFieldToken.TokenType.COLON, Char.ToString(ch))); break;
							case ';': SkipChar(); Tokens.Enqueue(new StructuredFieldToken(StructuredFieldToken.TokenType.SEMICOLON, Char.ToString(ch))); break;
							case '/': SkipChar(); Tokens.Enqueue(new StructuredFieldToken(StructuredFieldToken.TokenType.SLASH, Char.ToString(ch))); break;
							case '(': ParseComment(); break;
							case '[': ParseDomainLiteral(); break;
							case '<': ParseRoutingInformation(); break;
							case '"': ParseQuotedString(); break;
							case '\t':
							case ' ': ParseLWS(); break;
							case '\r': ParseCRLF(); break;
							default:
								if (0 < (short)ch && (short)ch < 127)
									ParseAtom(); 
								else
									throw new ApplicationException("Unexpected character '" + ch + "'");
								break;
						}
					}
					else
						Tokens.Enqueue(new StructuredFieldToken(StructuredFieldToken.TokenType.EOF));
				while (Tokens.Count == 0 && !IsEndOfStream());
			return Tokens.Dequeue();
		} // end of NextToken

		public StructuredFieldToken ParseThis(StructuredFieldToken.TokenType ttExpected)
		{
			return base.ParseThis(ttExpected);
		}
	} // end of StructuredFieldLexer
}
